/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package org.meanzoft.m3d.font;

import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.FontMetrics;
import java.awt.image.BufferedImage;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

import org.lwjgl.opengl.GL11;
import org.meanzoft.m3d.resource.ResourceManager;
import org.meanzoft.m3d.texture.GLImage;

/**
 * GLFont uses the Java Font class to create character sets and render text.  
 * GLFont can generate text in nearly any size, and in any font that Java supports.  
 * <P>
 */
public class GLFont {
    // the font that was used to generate character set

    Font font;
    // display list base for characters
    int fontListBase = -1;
    // texture containing a grid of 100 printable characters
    int fontTextureHandle = 0;
    // these will be set by getFontImageSize() and used by buildFont()
    int fontSize = 0;
    int textureSize = 0;
    // temp values for buildFont()
    static int[] charwidths = new int[100];

    /**
     * Dynamically create a texture mapped character set with the given Font.  
     * Text color will be white on a transparent background.
     * 
     * @param f   Java Font object
     */
    public GLFont(Font f) {
        makeFont(f, new float[]{1, 1, 1, 1}, new float[]{0, 0, 0, 0});
    }

    /**
     * Create a texture mapped character set with the given Font,  
     * Text color and background color.
     * 
     * @param f        Java Font object
     * @param fgColor  foreground (text) color as rgb or rgba values in range 0-1
     * @param bgColor  background color as rgb or rgba values in range 0-1 (set alpha to 0 to make transparent)
     */
    public GLFont(Font f, float[] fgColor, float[] bgColor) {
        makeFont(f, fgColor, bgColor);
    }

    /**
     * Return the handle to the texture holding the character set.
     */
    public int getTexture() {
        return fontTextureHandle;
    }

    /**
     * Prepare a texture mapped character set with the given Font, text color and background color.
     * Characters will be textured onto quads and stored in display lists.  The base display 
     * list id is stored in the fontListBase variable.  After makeFont() is run the print()
     * function can be used to render text in this font.
     * 
     * @param f        the font to draw characters
     * @param fgColor  foreground (text) color as rgb or rgba values in range 0-1
     * @param bgColor  background color as rgb or rgba values in range 0-1 (set alpha to 0 to make transparent)
     *
     * @see createFontImage()
     * @see print()
     */
    public void makeFont(Font f, float[] fgColor, float[] bgColor) {
        int charsetTexture = 0;
        if ((charsetTexture = makeFontTexture(f, fgColor, bgColor)) > 0) {
            // create 100 display lists, one for each character
            // textureSize and fontSize are calculated by createFontImage()
            buildFont(charsetTexture, textureSize, fontSize);
            fontTextureHandle = charsetTexture;
            font = f;
        }
    }

    /**
     * Return a texture containing a character set with the given Font arranged in
     * a 10x10 grid of printable characters.
     * 
     * @param f        the font to draw characters
     * @param fgColor  foreground (text) color as rgb or rgba values in range 0-1
     * @param bgColor  background color as rgb or rgba values in range 0-1 (set alpha to 0 to make transparent)
     * @see createFontImage()
     * @see print()
     */
    public int makeFontTexture(Font f, float[] fgColor, float[] bgColor) {
        int texture = 0;
        try {
            // Create a BufferedImage containing a 10x10 grid of printable characters
            BufferedImage image = createFontImage(
                    f, // the font 
                    fgColor, // text color
                    bgColor);   // background color 
            // make a texture with the buffered image
            int[] pixelsARGB = GLImage.getImagePixels(image);
            texture = makeTexture(convertImagePixelsRGBA(pixelsARGB, image.getWidth(), image.getHeight(), true), image.getWidth(), image.getHeight(), false);
        } catch (Exception e) {
            System.out.println("makeChar(): exception " + e);
        }
        return texture;
    }

    /**
     * Convert ARGB pixels to a ByteBuffer containing RGBA pixels.  The GL_RGBA format is
     * a default format used in OpenGL 1.0, but requires that we move the Alpha byte for
     * each pixel in the image (slow).  Would be better to use OpenGL 1.2 GL_BGRA format
     * and leave pixels in the ARGB format (faster) but this pixel format caused problems
     * when creating mipmaps (see note above).
     * .<P>
     * If flipVertically is true, pixels will be flipped vertically (for OpenGL coord system).
     * @return ByteBuffer
     */
    public static ByteBuffer convertImagePixelsRGBA(int[] jpixels, int imgw, int imgh, boolean flipVertically) {
        byte[] bytes;     // will hold pixels as RGBA bytes
        if (flipVertically) {
            jpixels = flipPixels(jpixels, imgw, imgh); // flip Y axis
        }
        bytes = convertARGBtoRGBA(jpixels);
        return allocBytes(bytes);  // convert to ByteBuffer and return
    }

    /**
     * Same function as in GLApp.java.  Allocates a ByteBuffer to hold the given
     * array of bytes.
     *
     * @param bytearray
     * @return  ByteBuffer containing the contents of the byte array
     */
    public static ByteBuffer allocBytes(byte[] bytearray) {
        ByteBuffer bb = ByteBuffer.allocateDirect(bytearray.length).order(ByteOrder.nativeOrder());
        bb.put(bytearray).flip();
        return bb;
    }

    /**
     * Flip an array of pixels vertically
     * @param imgPixels
     * @param imgw
     * @param imgh
     * @return int[]
     */
    public static int[] flipPixels(int[] imgPixels, int imgw, int imgh) {
        int[] flippedPixels = null;
        if (imgPixels != null) {
            flippedPixels = new int[imgw * imgh];
            for (int y = 0; y < imgh; y++) {
                for (int x = 0; x < imgw; x++) {
                    flippedPixels[ ((imgh - y - 1) * imgw) + x] = imgPixels[ (y * imgw) + x];
                }
            }
        }
        return flippedPixels;
    }

    /**
     * Convert pixels from java default ARGB int format to byte array in RGBA format.
     * @param jpixels
     * @return
     */
    public static byte[] convertARGBtoRGBA(int[] jpixels) {
        byte[] bytes = new byte[jpixels.length * 4];  // will hold pixels as RGBA bytes
        int p, r, g, b, a;
        int j = 0;
        for (int i = 0; i < jpixels.length; i++) {
            p = jpixels[i];
            a = (p >> 24) & 0xFF;  // get pixel bytes in ARGB order
            r = (p >> 16) & 0xFF;
            g = (p >> 8) & 0xFF;
            b = (p >> 0) & 0xFF;
            bytes[j + 0] = (byte) r;  // fill in bytes in RGBA order
            bytes[j + 1] = (byte) g;
            bytes[j + 2] = (byte) b;
            bytes[j + 3] = (byte) a;
            j += 4;
        }
        return bytes;
    }

    public static int makeTexture(ByteBuffer pixels, int w, int h, boolean anisotropic) {
        // get a new empty texture
        int textureHandle = ResourceManager.getManager().getTextureLoader().createTextureID();
        // preserve currently bound texture, so glBindTexture() below won't affect anything)
        GL11.glPushAttrib(GL11.GL_TEXTURE_BIT);
        // 'select' the new texture by it's handle
        GL11.glBindTexture(GL11.GL_TEXTURE_2D, textureHandle);
        // set texture parameters
        GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_REPEAT);
        GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_REPEAT);
        GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR); //GL11.GL_NEAREST);
        GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); //GL11.GL_NEAREST);

        // Create the texture from pixels
        GL11.glTexImage2D(GL11.GL_TEXTURE_2D,
                0, // level of detail
                GL11.GL_RGBA8, // internal format for texture is RGB with Alpha
                w, h, // size of texture image
                0, // no border
                GL11.GL_RGBA, // incoming pixel format: 4 bytes in RGBA order
                GL11.GL_UNSIGNED_BYTE, // incoming pixel data type: unsigned bytes
                pixels);				// incoming pixels

        // restore previous texture settings
        GL11.glPopAttrib();

        return textureHandle;
    }

    /**
     * Return a texture containing the given single character with the Courier font.  
     * TO DO: pass Font as a parameter.
     * 
     * @param onechar     character to draw into texture
     */
    public static int makeCharTexture(Font f, String onechar, float[] fgColor, float[] bgColor) {
        int texture = 0;
        try {
            // Create a BufferedImage with one character
            BufferedImage image = createCharImage(onechar,
                    f, // the font
                    fgColor, // text
                    bgColor);      // background
            // make a texture from the image
            int[] pixelsARGB = GLImage.getImagePixels(image);
            texture = makeTexture(convertImagePixelsRGBA(pixelsARGB, image.getWidth(), image.getHeight(), true), image.getWidth(), image.getHeight(), false);
        } catch (Exception e) {
            System.out.println("makeChar(): exception " + e);
        }
        return texture;
    }

    /**
     * return a BufferedImage containing the given character drawn with the given font.
     * Character will be drawn on its baseline, and centered horizontally in the image.
     * 
     * @param text     a single character to render
     * @param font     the font to render with
     * @param fgColor  foreground (text) color as rgb or rgba values in range 0-1
     * @param bgColor  background color as rgb or rgba values in range 0-1 (set alpha to 0 to make transparent)
     * @return
     */
    public static BufferedImage createCharImage(String text, Font font, float[] fgColor, float[] bgColor) {
        Color bg = bgColor == null ? new Color(0, 0, 0, 0) : (bgColor.length == 3 ? new Color(bgColor[0], bgColor[1], bgColor[2], 1) : new Color(bgColor[0], bgColor[1], bgColor[2], bgColor[3]));
        Color fg = fgColor == null ? new Color(1, 1, 1, 1) : (fgColor.length == 3 ? new Color(fgColor[0], fgColor[1], fgColor[2], 1) : new Color(fgColor[0], fgColor[1], fgColor[2], fgColor[3]));
        boolean isAntiAliased = true;
        boolean usesFractionalMetrics = false;

        // get size of texture image neaded to hold largest character of this font
        int maxCharSize = getFontSize(font);
        int imgSize = GLImage.getPowerOfTwoBiggerThan(maxCharSize);
        if (imgSize > 2048) {
            System.err.println("GLFont.createCharImage(): texture size will be too big (" + imgSize + ") Make the font size smaller.");
            return null;
        }

        // we'll draw text into this image
        BufferedImage image = new BufferedImage(imgSize, imgSize, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = image.createGraphics();

        // Clear image with background color (make transparent if color has alpha value)
        if (bg.getAlpha() < 255) {
            g.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR, (float) bg.getAlpha() / 255f));
        }
        g.setColor(bg);
        g.fillRect(0, 0, imgSize, imgSize);

        // prepare to draw character in foreground color
        g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f));
        g.setColor(fg);
        g.setFont(font);
        g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, isAntiAliased ? RenderingHints.VALUE_TEXT_ANTIALIAS_ON : RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
        g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, usesFractionalMetrics ? RenderingHints.VALUE_FRACTIONALMETRICS_ON : RenderingHints.VALUE_FRACTIONALMETRICS_OFF);
        g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);

        // place the character (on baseline, centered horizontally)
        FontMetrics fm = g.getFontMetrics();
        int cwidth = fm.charWidth(text.charAt(0));
        int height = fm.getHeight();
        int ascent = fm.getAscent();
        int vborder = (int) ((float) (imgSize - height) / 2f);
        int hborder = (int) ((float) (imgSize - cwidth) / 2f);
        g.drawString(text, hborder, vborder + ascent);
        g.dispose();

        return image;
    }

    /**
     * Return a BufferedImage containing 100 printable characters drawn with the given font.  Characters
     * will be arranged in a 10x10 grid.
     * @param text
     * @param font
     * @param imgSize  a power of two (32 64 256 etc)
     * @param fgColor  foreground (text) color as rgb or rgba values in range 0-1
     * @param bgColor  background color as rgb or rgba values in range 0-1 (set alpha to 0 to make transparent)
     * @return
     */
    public BufferedImage createFontImage(Font font, float[] fgColor, float[] bgColor) {
        Color bg = bgColor == null ? new Color(0, 0, 0, 0) : (bgColor.length == 3 ? new Color(bgColor[0], bgColor[1], bgColor[2], 1) : new Color(bgColor[0], bgColor[1], bgColor[2], bgColor[3]));
        Color fg = fgColor == null ? new Color(1, 1, 1, 1) : (fgColor.length == 3 ? new Color(fgColor[0], fgColor[1], fgColor[2], 1) : new Color(fgColor[0], fgColor[1], fgColor[2], fgColor[3]));
        boolean isAntiAliased = true;
        boolean usesFractionalMetrics = false;

        // get size of texture image neaded to hold 10x10 character grid
        fontSize = getFontSize(font);
        textureSize = GLImage.getPowerOfTwoBiggerThan(fontSize * 10);
        //System.out.println("GLFont.getFontImageSize(): build font with fontsize=" + fontSize + " gridsize=" + (fontSize * 10) + " texturesize=" + textureSize);
        if (textureSize > 2048) {
            System.err.println("GLFont.createFontImage(): texture size will be too big (" + textureSize + ") Make the font size smaller.");
            return null;
        }

        // create a buffered image to hold charset
        BufferedImage image = new BufferedImage(textureSize, textureSize, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = image.createGraphics();

        // Clear image with background color (make transparent if color has alpha value)
        if (bg.getAlpha() < 255) {
            g.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR, (float) bg.getAlpha() / 255f));
        }
        g.setColor(bg);
        g.fillRect(0, 0, textureSize, textureSize);

        // prepare to draw characters in foreground color
        g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f));
        g.setColor(fg);
        g.setFont(font);
        g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, isAntiAliased ? RenderingHints.VALUE_TEXT_ANTIALIAS_ON : RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
        g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, usesFractionalMetrics ? RenderingHints.VALUE_FRACTIONALMETRICS_ON : RenderingHints.VALUE_FRACTIONALMETRICS_OFF);
        g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);

        // get font measurements
        FontMetrics fm = g.getFontMetrics();
        int ascent = fm.getMaxAscent();

        // draw the grid of 100 characters
        for (int r = 0; r < 10; r++) {
            for (int c = 0; c < 10; c++) {
                char ch = (char) (32 + ((r * 10) + c));
                g.drawString(String.valueOf(ch), (c * fontSize), (r * fontSize) + ascent);
                charwidths[(r * 10) + c] = fm.charWidth(ch);
            }
        }
        g.dispose();

        return image;
    }

    /**
     * Return the maximum character size of the given Font. This will be the max of 
     * the vertical and horizontal font dimensions, so can be used to create a square
     * image large enough to hold any character rendered with this Font.  
     * <P>
     * Creates a BufferedImage and Graphics2D graphics context to get font sizes (is 
     * there a more efficient way to do this?).
     * <P>
     * @param font  Font object describes the font to render with 
     * @return  power-of-two texture size large enough to hold the character set
     */
    public static int getFontSize(Font font) {
        boolean isAntiAliased = true;
        boolean usesFractionalMetrics = false;

        // just a dummy image so we can get a graphics context
        BufferedImage image = new BufferedImage(64, 64, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = image.createGraphics();

        // prepare to draw character
        g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f));
        g.setFont(font);
        g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, isAntiAliased ? RenderingHints.VALUE_TEXT_ANTIALIAS_ON : RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
        g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, usesFractionalMetrics ? RenderingHints.VALUE_FRACTIONALMETRICS_ON : RenderingHints.VALUE_FRACTIONALMETRICS_OFF);
        g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);

        // get character measurements
        FontMetrics fm = g.getFontMetrics();
        int ascent = fm.getMaxAscent();
        int descent = fm.getMaxDescent();
        int advance = fm.charWidth('W');  // width of widest char, more reliable than getMaxAdvance();
        int leading = fm.getLeading();

        // calculate size of 10x10 character grid 
        int fontHeight = ascent + descent + (leading / 2);
        int fontWidth = advance;
        int maxCharSize = Math.max(fontHeight, fontWidth);
        return maxCharSize;
    }

    /**
     * Build the character set display list from the given texture.  Creates
     * one quad for each character, with one letter textured onto each quad.
     * Assumes the texture is a 256x256 image containing every
     * character of the charset arranged in a 16x16 grid.  Each character
     * is 16x16 pixels.  Call destroyFont() to release the display list memory.
     *
     * Should be in ORTHO (2D) mode to render text (see setOrtho()).
     *
     * Special thanks to NeHe and Giuseppe D'Agata for the "2D Texture Font"
     * tutorial (http://nehe.gamedev.net).
     *
     * @param charSetImage   texture image containing 100 characters in a 10x10 grid
     * @param fontWidth      how many pixels to allow per character on screen
     *
     * @see       destroyFont()
     */
    public void buildFont(int fontTxtrHandle, int textureSize, int fontSize) {
        int unitSize = fontSize;          // pixel size of one block in 10x10 grid
        float usize = (float) unitSize / (float) (textureSize);  // UV size of one block in grid
        float chU, chV;         // character UV position

        // Create 100 Display Lists
        fontListBase = GL11.glGenLists(100);

        // make a quad for each character in texture
        for (int i = 0; i < 100; i++) {
            int x = (i % 10);  // column
            int y = (i / 10);  // row

            // make character UV coordinate 
            // the character V position is tricky because we have to invert the V coord 
            // (char # 0 is at top of texture image, but V 0 is at bottom)
            chU = (float) (x * unitSize) / (float) textureSize;
            chV = (float) (textureSize - (y * unitSize) - unitSize) / (float) textureSize;

            GL11.glNewList(fontListBase + i, GL11.GL_COMPILE); // start display list
            {
                GL11.glBegin(GL11.GL_QUADS);           // Make A unitSize square quad 
                {
                    GL11.glTexCoord2f(chU, chV);          // Texture Coord (Bottom Left)
                    GL11.glVertex2i(0, 0);

                    GL11.glTexCoord2f(chU + usize, chV);      // Texture Coord (Bottom Right)
                    GL11.glVertex2i(unitSize, 0);

                    GL11.glTexCoord2f(chU + usize, chV + usize);  // Texture Coord (Top Right)
                    GL11.glVertex2i(unitSize, unitSize);

                    GL11.glTexCoord2f(chU, chV + usize);     // Texture Coord (Top Left)
                    GL11.glVertex2i(0, unitSize);

                }
                GL11.glEnd();
                GL11.glTranslatef(charwidths[i], 0, 0);   // shift right the width of the character
            }
            GL11.glEndList();    // done display list
        }
    }

    /**
     * Clean up the allocated display lists for the character set.
     */
    public void destroyFont() {
        if (fontListBase != -1) {
            GL11.glDeleteLists(fontListBase, 256);
            fontListBase = -1;
        }
    }

    /**
     * Set OpenGL to render in flat 2D (no perspective) on top of current scene.
     * Preserve current projection and model views, and disable depth testing.
     * Ortho world size will be same as viewport size, so any ortho drawing
     * (drawQuad(), drawImageFullscreen(), etc.) will be scaled to fit viewport.
     * <P>
     * NOTE: if the viewport is the same size as the window (by default it is),
     * then setOrtho() will make the world coordinates exactly match the screen
     * pixel positions.  This is convenient for mouse interaction, but be warned:
     * if you setViewport() to something other than fullscreen, then you need
     * to use getWorldCoordsAtScreen() to convert screen xy to world xy.
     * <P>
     * Once Ortho is on, glTranslate() will take pixel coords as arguments,
     * with the lower left corner 0,0 and the upper right corner 1024,768 (or
     * whatever your screen size is).  !!!
     *
     * @see setOrthoOff()
     * @see setViewport(int,int,int,int)
     */
    public static void setOrthoOn() {
        // prepare projection matrix to render in 2D
        GL11.glMatrixMode(GL11.GL_PROJECTION);
        GL11.glPushMatrix();                   // preserve perspective view
        GL11.glLoadIdentity();                 // clear the perspective matrix
        GL11.glOrtho( // turn on 2D mode
                ////viewportX,viewportX+viewportW,    // left, right
                ////viewportY,viewportY+viewportH,    // bottom, top    !!!
                0, 800, // left, right
                0, 640, // bottom, top
                -500, 500);                        // Zfar, Znear
        // clear the modelview matrix
        GL11.glMatrixMode(GL11.GL_MODELVIEW);
        GL11.glPushMatrix();				   // Preserve the Modelview Matrix
        GL11.glLoadIdentity();				   // clear the Modelview Matrix
        // disable depth test so further drawing will go over the current scene
        GL11.glDisable(GL11.GL_DEPTH_TEST);
    }

    /**
     * Turn 2D mode off.  Return the projection and model views to their
     * preserved state that was saved when setOrthoOn() was called, and
     * re-enable depth testing.
     *
     * @see setOrthoOn()
     */
    public static void setOrthoOff() {
        // restore the original positions and views
        GL11.glMatrixMode(GL11.GL_PROJECTION);
        GL11.glPopMatrix();
        GL11.glMatrixMode(GL11.GL_MODELVIEW);
        GL11.glPopMatrix();
        // turn Depth Testing back on
        GL11.glEnable(GL11.GL_DEPTH_TEST);
    }

    /**
     * Render a text string in 2D over the scene, using the character set created
     * by this GLFont object.
     * 
     * @see makeFont()
     */
    public void print(int x, int y, String msg) {
        if (msg != null) {
            // preserve current GL settings
            GL11.glPushAttrib(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_TEXTURE_BIT | GL11.GL_LIGHTING_BIT | GL11.GL_DEPTH_BUFFER_BIT);
            {
                // turn off lighting
                GL11.glDisable(GL11.GL_LIGHTING);
                // enable alpha blending, so character background is transparent
                GL11.glEnable(GL11.GL_BLEND);
                GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
                // enable the charset texture
                GL11.glBindTexture(GL11.GL_TEXTURE_2D, fontTextureHandle);
                // prepare to render in 2D
                setOrthoOn();
                // draw the text
                GL11.glTranslatef(x, y, 0);        // Position The Text (in pixel coords)
                for (int i = 0; i < msg.length(); i++) {
                    GL11.glCallList(fontListBase + (msg.charAt(i) - 32));
                }
                // restore the original positions and views
                setOrthoOff();
            }
            GL11.glPopAttrib();  // restore previous settings
        }
    }
}
